using System;
using System.Collections.Specialized;
using System.Configuration;
using System.Configuration.Provider;
using System.Security.Cryptography;
using System.Text;
using System.Web.Configuration;
using System.Web.Hosting;
using System.Web.Security;
using Webbness.Mvc.Models;
using Webbness.Mvc.SubSonic;

namespace Webbness.Mvc.Security
{
    public class SimpleMembershipProvider<TUser, TJoin, TRole> : MembershipProvider, IEncryptionProvider
        where TUser : SecurityUser<TUser, TJoin, TRole>, new()
        where TRole : SecurityRole<TRole, TJoin, TUser>, new()
        where TJoin : SecurityUserInRole<TRole, TJoin, TUser>, new()
    {
        private const int newPasswordLength = 8;
        private UserRepository<TUser, TJoin, TRole> repository;
        private string applicationName;
        private bool enablePasswordReset;
        private bool enablePasswordRetrieval;
        private MachineKeySection machineKey;
        private int maxInvalidPasswordAttempts;
        private int minRequiredNonAlphanumericCharacters;
        private int minRequiredPasswordLength;
        private int passwordAttemptWindow;
        private MembershipPasswordFormat passwordFormat;
        private string passwordStrengthRegularExpression;
        private bool requiresQuestionAndAnswer;
        private bool requiresUniqueEmail;

        #region Enums

        private enum FailureType
        {
            Password = 1,
            PasswordAnswer = 2
        }

        #endregion



        #region Initialization

        public override void Initialize(string name, NameValueCollection config)
        {
            if (config == null)
            {
                throw new ArgumentNullException("config");
            }

            if (String.IsNullOrEmpty(name))
            {
                name = "SimpleMembershipProvider";
            }

            if (String.IsNullOrEmpty(config["description"]))
            {
                config.Remove("description");
                config.Add("description", "Base Membership provider");
            }

            //Initialize the abstract base class.
            base.Initialize(name, config);

            setup(config);

            repository = new UserRepository<TUser, TJoin, TRole>();
        }

        private void setup(NameValueCollection config)
        {
            applicationName = GetConfigValue(config["applicationName"], HostingEnvironment.ApplicationVirtualPath);
            maxInvalidPasswordAttempts = Convert.ToInt32(GetConfigValue(config["maxInvalidPasswordAttempts"], "5"));
            passwordAttemptWindow = Convert.ToInt32(GetConfigValue(config["passwordAttemptWindow"], "10"));
            minRequiredNonAlphanumericCharacters = Convert.ToInt32(GetConfigValue(config["minRequiredAlphaNumericCharacters"], "1"));
            minRequiredPasswordLength = Convert.ToInt32(GetConfigValue(config["minRequiredPasswordLength"], "7"));
            passwordStrengthRegularExpression = Convert.ToString(GetConfigValue(config["passwordStrengthRegularExpression"], String.Empty));
            enablePasswordReset = Convert.ToBoolean(GetConfigValue(config["enablePasswordReset"], "true"));
            enablePasswordRetrieval = Convert.ToBoolean(GetConfigValue(config["enablePasswordRetrieval"], "true"));
            requiresQuestionAndAnswer = Convert.ToBoolean(GetConfigValue(config["requiresQuestionAndAnswer"], "false"));
            requiresUniqueEmail = Convert.ToBoolean(GetConfigValue(config["requiresUniqueEmail"], "true"));

            string temp_format = config["passwordFormat"] ?? "Hashed";

            switch (temp_format)
            {
                case "Hashed":
                    passwordFormat = MembershipPasswordFormat.Hashed;
                    break;
                case "Encrypted":
                    passwordFormat = MembershipPasswordFormat.Encrypted;
                    break;
                case "Clear":
                    passwordFormat = MembershipPasswordFormat.Clear;
                    break;
                default:
                    throw new ProviderException("Password format not supported.");
            }

            //Get encryption and decryption key information from the configuration.
            Configuration cfg = WebConfigurationManager.OpenWebConfiguration(HostingEnvironment.ApplicationVirtualPath);
            machineKey = cfg.GetSection("system.web/machineKey") as MachineKeySection;

            if (machineKey == null)
                throw new ConfigurationErrorsException("system.web/machineKey");

            if (machineKey.ValidationKey.Contains("AutoGenerate"))
            {
                if (PasswordFormat != MembershipPasswordFormat.Clear)
                {
                    throw new ProviderException("Hashed or Encrypted passwords are not supported with auto-generated keys.");
                }
            }
        }

        private string GetConfigValue(string configValue, string defaultValue)
        {
            if (String.IsNullOrEmpty(configValue))
            {
                return defaultValue;
            }

            return configValue;
        }

        #endregion

        public override MembershipUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status)
        {
            var args = new ValidatePasswordEventArgs(username, password, true);

            OnValidatingPassword(args);

            if (args.Cancel)
            {
                status = MembershipCreateStatus.InvalidPassword;
                return null;
            }

            if ((RequiresUniqueEmail && (GetUserNameByEmail(email) != String.Empty)))
            {
                status = MembershipCreateStatus.DuplicateEmail;
                return null;
            }

            if (RequiresQuestionAndAnswer && (String.IsNullOrEmpty(passwordQuestion) || String.IsNullOrEmpty(passwordAnswer)))
            {
                status = MembershipCreateStatus.InvalidQuestion;
                return null;
            }

            MembershipUser membershipUser = GetUser(username, false);

            if (membershipUser == null)
            {
                DateTime createDate = DateTime.Now;

                var user = repository.GetNew();

                user.UserName = username;
                user.Password = EncodePassword(password);
                //user.ApplicationName = applicationName;
                user.Email = email;
                if (RequiresQuestionAndAnswer)
                {
                    user.PasswordQuestion = passwordQuestion;
                    user.PasswordAnswer = EncodePassword(passwordAnswer);
                }
                else
                {
                    user.PasswordQuestion = String.Empty;
                    user.PasswordAnswer = String.Empty;
                }
                user.IsApproved = isApproved;
                user.CreationDate = createDate;
                user.LastActivityDate = createDate;
                user.LastLockedOutDate = createDate;
                user.LastLoginDate = createDate;
                user.FailedPasswordAnswerAttemptWindowStart = createDate;
                user.FailedPasswordAttemptWindowStart = createDate;
                user.LastPasswordChangedDate = createDate;
                user.Comment = string.Empty;

                try
                {
                    repository.Add(user);
                    //repository.Save();
                    status = MembershipCreateStatus.Success;
                }
                catch
                {
                    //Add exception handling here.

                    status = MembershipCreateStatus.ProviderError;
                }


                return GetUser(username, false);
            }
            
                status = MembershipCreateStatus.DuplicateUserName;
           
            return null;
        }

        public string EncodePassword(string password)
        {
            string encodedPassword = password;

            switch (PasswordFormat)
            {
                case MembershipPasswordFormat.Clear:
                    break;
                case MembershipPasswordFormat.Encrypted:
                    encodedPassword =
                        Convert.ToBase64String(EncryptPassword(Encoding.Unicode.GetBytes(password)));
                    break;
                case MembershipPasswordFormat.Hashed:
                    var hash = new HMACSHA1();
                    hash.Key = HexToByte(machineKey.ValidationKey);
                    encodedPassword =
                        Convert.ToBase64String(hash.ComputeHash(Encoding.Unicode.GetBytes(password)));
                    break;
                default:
                    throw new ProviderException("Unsupported password format.");
            }

            return encodedPassword;
        }

        /// <summary>
        /// Converts a hexadecimal string to a byte array. Used to convert encryption key values from the configuration
        /// </summary>
        /// <param name="hexString"></param>
        /// <returns></returns>
        /// <remarks></remarks>
        private byte[] HexToByte(string hexString)
        {
            var returnBytes = new byte[hexString.Length/2];
            for (int i = 0; i < returnBytes.Length; i++)
                returnBytes[i] = Convert.ToByte(hexString.Substring(i*2, 2), 16);
            return returnBytes;
        }

        public override bool ChangePasswordQuestionAndAnswer(string username, string password, string newPasswordQuestion, string newPasswordAnswer)
        {
            if (!ValidateUser(username, password))
            {
                return false;
            }

            var user = repository.GetUserByUserName(username);
            user.PasswordQuestion = newPasswordQuestion;
            user.PasswordAnswer = EncodePassword(newPasswordAnswer);
            repository.Update(user);
            return true;
        }

        public override string GetPassword(string username, string answer)
        {
            if (!EnablePasswordRetrieval)
            {
                throw new ProviderException("Password Retrieval Not Enabled.");
            }

            if (PasswordFormat == MembershipPasswordFormat.Hashed)
            {
                throw new ProviderException("Cannot retrieve Hashed passwords.");
            }

            var user = repository.GetUserByUserName(username);

            if (user != null)
            {
                if (user.IsLockedOut)
                {
                    throw new MembershipPasswordException("The supplied user is locked out.");
                }
            }
            else
            {
                throw new MembershipPasswordException("The supplied user name is not found.");
            }
            if (RequiresQuestionAndAnswer && !CheckPassword(answer, user.PasswordAnswer))
            {
                UpdateFailureCount(username, FailureType.PasswordAnswer);

                throw new MembershipPasswordException("Incorrect password answer.");
            }

            if (PasswordFormat == MembershipPasswordFormat.Encrypted)
            {
                return UnEncodePassword(user.Password);
            }

            return user.Password;
        }

        public override bool ChangePassword(string username, string oldPwd, string newPwd)
        {
            if (!ValidateUser(username, oldPwd))
            {
                return false;
            }

            var args = new ValidatePasswordEventArgs(username, newPwd, true);

            OnValidatingPassword(args);

            if (args.Cancel)
            {
                if (args.FailureInformation != null)
                {
                    throw args.FailureInformation;
                }
                else
                {
                    throw new Exception("Change password canceled due to new password validation failure.");
                }
            }

            var user = repository.GetUserByUserName(username);
            user.Password = EncodePassword(newPwd);


            repository.Update(user);

            return true;
        }

        public override string ResetPassword(string username, string answer)
        {
            if (!EnablePasswordReset)
            {
                throw new NotSupportedException("Password Reset is not enabled.");
            }

            if ((answer == null) && (RequiresQuestionAndAnswer))
            {
                UpdateFailureCount(username, FailureType.PasswordAnswer);

                throw new ProviderException("Password answer required for password Reset.");
            }

            string newPassword =
                Membership.GeneratePassword(
                    newPasswordLength,
                    MinRequiredNonAlphanumericCharacters
                    );

            var args = new ValidatePasswordEventArgs(username, newPassword, true);

            OnValidatingPassword(args);

            if (args.Cancel)
            {
                if (args.FailureInformation != null)
                {
                    throw args.FailureInformation;
                }
                else
                {
                    throw new MembershipPasswordException("Reset password canceled due to password validation failure.");
                }
            }


            //int rowsAffected = 0;
            string passwordAnswer = String.Empty;

            var user = repository.GetUserByUserName(username);


            if (user != null)
            {
                if (user.IsLockedOut)
                {
                    throw new MembershipPasswordException("The supplied user is locked out.");
                }

                passwordAnswer = user.PasswordAnswer;
            }
            else
            {
                throw new MembershipPasswordException("The supplied user name is not found.");
            }

            if (RequiresQuestionAndAnswer && (!CheckPassword(answer, passwordAnswer)))
            {
                UpdateFailureCount(username, FailureType.PasswordAnswer);

                throw new MembershipPasswordException("Incorrect password answer.");
            }

            user.Password = EncodePassword(newPassword);


            return newPassword;
        }

        public override void UpdateUser(MembershipUser user)
        {
            var u = repository.GetUserByUserName(user.UserName);
            if (u == null)
                return;

            u.Email = user.Email;
            u.IsApproved = user.IsApproved;
            u.UserName = user.UserName;

            repository.Update(u);
        }

        public override bool ValidateUser(string username, string password)
        {
            var user = repository.GetUserByUserName(username);
            if (user == null)
                return false;
            return user.Password.Equals(EncodePassword(password));
        }

        public override bool UnlockUser(string userName)
        {
            var user = repository.GetUserByUserName(userName);
            user.IsLockedOut = false;
            repository.Update(user);
            return true;
        }

        public override MembershipUser GetUser(object providerUserKey, bool userIsOnline)
        {
            int userid = Convert.ToInt32(providerUserKey);
            var user = repository.GetUserByUserID(userid);
            MembershipUser membershipUser = ConvertToMembershipUser(user);

            if (membershipUser != null)
            {
                if (userIsOnline)
                {
                    user.LastActivityDate = DateTime.Now;
                    repository.Update(user);
                }
            }

            return membershipUser;
        }


        public override MembershipUser GetUser(string username, bool userIsOnline)
        {
            var user = repository.GetUserByUserName(username);
            MembershipUser membershipUser = ConvertToMembershipUser(user);

            if (membershipUser != null)
            {
                if (userIsOnline)
                {
                    user.LastActivityDate = DateTime.Now;
                    repository.Update(user);
                }
            }

            return membershipUser;
        }

        private MembershipUser ConvertToMembershipUser(TUser user)
        {
            MembershipUser u = null;
            if (user != null)
            {
                u = new MembershipUser(
                    Name,
                    user.UserName,
                    user.Id,
                    user.Email,
                    user.PasswordQuestion,
                    "",
                    user.IsApproved ,
                    user.IsLockedOut ,
                    user.CreationDate ,
                    user.LastLoginDate ,
                    user.LastActivityDate ,
                    user.LastPasswordChangedDate ,
                    user.LastLockedOutDate 
                    );
            }
            return u;
        }

        public override string GetUserNameByEmail(string email)
        {
            var user = repository.GetUserByEmail(email);
            return user == null ? "" : user.UserName;
        }

        public override bool DeleteUser(string username, bool deleteAllRelatedData)
        {
            var user = repository.GetUserByUserName(username);
            if (user == null)
                return false;
            repository.Delete(user);
            return true;
        }

        public override MembershipUserCollection GetAllUsers(int pageIndex, int pageSize, out int totalRecords)
        {
            var users = new MembershipUserCollection();
            var us = repository.FindAllUsers();

            totalRecords = us.Count;


            int counter = 0;
            int startIndex = pageSize*pageIndex;
            int endIndex = startIndex + pageSize - 1;

            foreach (var user in us)
            {
                if (counter >= startIndex)
                {
                    users.Add(ConvertToMembershipUser(user));
                }

                if (counter >= endIndex)
                {
                    continue;
                }
                counter += 1;
            }


            return users;
        }

        public override int GetNumberOfUsersOnline()
        {
            return repository.GetNumberOfUsersOnline();
        }

        public override MembershipUserCollection FindUsersByName(string usernameToMatch, int pageIndex, int pageSize, out int totalRecords)
        {
            var users = new MembershipUserCollection();
            var us = repository.FindMatchingUsers(usernameToMatch);

            totalRecords = us.Count;


            int counter = 0;
            int startIndex = pageSize*pageIndex;
            int endIndex = startIndex + pageSize - 1;

            foreach (var user in us)
            {
                if (counter >= startIndex)
                {
                    users.Add(ConvertToMembershipUser(user));
                }

                if (counter >= endIndex)
                {
                    continue;
                }
                counter += 1;
            }


            return users;
        }

        public override MembershipUserCollection FindUsersByEmail(string emailToMatch, int pageIndex, int pageSize, out int totalRecords)
        {
            var users = new MembershipUserCollection();
            var us = repository.FindMatchingEmails(emailToMatch);

            totalRecords = us.Count;


            int counter = 0;
            int startIndex = pageSize*pageIndex;
            int endIndex = startIndex + pageSize - 1;

            foreach (var user in us)
            {
                if (counter >= startIndex)
                {
                    users.Add(ConvertToMembershipUser(user));
                }

                if (counter >= endIndex)
                {
                    continue;
                }
                counter += 1;
            }


            return users;
        }

        /// <summary>
        /// UnEncode password.
        /// </summary>
        /// <param name="encodedPassword">Password.</param>
        /// <returns>Unencoded password.</returns>
        public string UnEncodePassword(string encodedPassword)
        {
            string password = encodedPassword;

            switch (PasswordFormat)
            {
                case MembershipPasswordFormat.Clear:
                    break;
                case MembershipPasswordFormat.Encrypted:
                    password =
                        Encoding.Unicode.GetString(DecryptPassword(Convert.FromBase64String(password)));
                    break;
                case MembershipPasswordFormat.Hashed:
                    throw new ProviderException("Cannot unencode a hashed password.");
                default:
                    throw new ProviderException("Unsupported password format.");
            }

            return password;
        }

        /// <summary>
        /// Update password and answer failure information.
        /// </summary>
        /// <param name="username">User name.</param>
        /// <param name="failureType">Type of failure</param>
        /// <remarks></remarks>
        private void UpdateFailureCount(string username, FailureType failureType)
        {
            var user = repository.GetUserByUserName(username);
            if (user == null)
                return;

            user.FailedPasswordAnswerAttemptCount = user.FailedPasswordAnswerAttemptCount + 1;

            repository.Update(user);
        }

        /// <summary>
        /// Check the password format based upon the MembershipPasswordFormat.
        /// </summary>
        /// <param name="password">Password</param>
        /// <param name="dbpassword"></param>
        /// <returns></returns>
        /// <remarks></remarks>
        private bool CheckPassword(string password, string dbpassword)
        {
            string pass1 = password;
            string pass2 = dbpassword;

            switch (PasswordFormat)
            {
                case MembershipPasswordFormat.Encrypted:
                    pass2 = UnEncodePassword(dbpassword);
                    break;
                case MembershipPasswordFormat.Hashed:
                    pass1 = EncodePassword(password);
                    break;
                default:
                    break;
            }

            if (pass1 == pass2)
            {
                return true;
            }

            return false;
        }

        #region Properties

        public override bool EnablePasswordRetrieval
        {
            get { return enablePasswordRetrieval; }
        }

        public override bool EnablePasswordReset
        {
            get { return enablePasswordReset; }
        }

        public override bool RequiresQuestionAndAnswer
        {
            get { return requiresQuestionAndAnswer; }
        }

        public override string ApplicationName
        {
            get { return applicationName; }
            set { applicationName = value; }
        }

        public override int MaxInvalidPasswordAttempts
        {
            get { return maxInvalidPasswordAttempts; }
        }

        public override int PasswordAttemptWindow
        {
            get { return passwordAttemptWindow; }
        }

        public override bool RequiresUniqueEmail
        {
            get { return requiresUniqueEmail; }
        }

        public override MembershipPasswordFormat PasswordFormat
        {
            get { return passwordFormat; }
        }

        public override int MinRequiredPasswordLength
        {
            get { return minRequiredPasswordLength; }
        }

        public override int MinRequiredNonAlphanumericCharacters
        {
            get { return minRequiredNonAlphanumericCharacters; }
        }

        public override string PasswordStrengthRegularExpression
        {
            get { return passwordStrengthRegularExpression; }
        }

        #endregion
    }
}